#include "st7567_generic_spi.h"

#if (USE_LCD_PANEL_ST7567_SPI_POC && LCD_GENERIC_SPI)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <stddef.h>
#include <string.h>
#include "lvgl/src/lv_core/lv_refr.h"
#include "lvgl/src/lv_core/lv_disp.h"
#include "lvgl/src/lv_hal/lv_hal_tick.h"
#include "crane_lcd_spi.h"
#include "st_lcd_spi_drv.h"
#include "../common/utils.h"
#include "../common/lv_pm.h"
#include "../../board.h"
#include "lv_conf.h"

static lv_disp_drv_t * lcd_disp_drv = NULL;

#define SPI_TRANSFER_MODE               XFER_MODE_DMA
#define GPIO_LCD_RST                     GPIO31

const static uint8_t pagemap[] = { 7, 6, 5, 4, 3, 2, 1, 0 };
const static unsigned st7567_id = 0x7567;

static const uint32_t mfp_lcd[] = {
  MFP_REG(GPIO_33) | MFP_AF1 | MFP_PULL_HIGH | MFP_SLEEP_DIR | MFP_DRIVE_MEDIUM | MFP_LPM_EDGE_NONE, /* SPI_CLK */
  MFP_REG(GPIO_34) | MFP_AF0 | MFP_PULL_HIGH | MFP_SLEEP_DIR | MFP_DRIVE_MEDIUM | MFP_LPM_EDGE_NONE, /* SPI_CS */
  MFP_REG(GPIO_35) | MFP_AF1 | MFP_PULL_HIGH | MFP_SLEEP_DIR | MFP_DRIVE_MEDIUM | MFP_LPM_EDGE_NONE, /* SPI_RXD / SDA */
  MFP_REG(GPIO_36) | MFP_AF1 | MFP_PULL_HIGH | MFP_SLEEP_DIR | MFP_DRIVE_MEDIUM | MFP_LPM_EDGE_NONE, /* SPI_TXD */
  MFP_REG(GPIO_31) | MFP_AF0 | MFP_PULL_HIGH | MFP_SLEEP_DIR | MFP_DRIVE_MEDIUM | MFP_LPM_EDGE_NONE, /* LCD_RST */
  MFP_REG(GPIO_121) | MFP_AF0 | MFP_PULL_HIGH | MFP_SLEEP_DIR | MFP_DRIVE_MEDIUM | MFP_LPM_EDGE_NONE, /* DCX */
  MFP_EOC
};
struct lcd_spi_info lcd_info = {
    .dev = SPI_BUS_SSP0,
    .spi_mode = SPI_MODE_0,
    .wclk = SPI_CLK_3250KHZ,
    .rclk = SPI_CLK_26MHZ,
    .cs_pin = GPIO34,
#if LCD_SPI_4_LINE
    .protocol = INTERFACE_PROTOCOL_SPI_4_LINE,
    .dc_pin = GPIO121,
#else
    .protocol = INTERFACE_PROTOCOL_SPI_3_LINE,
#endif
    .mfp = mfp_lcd,
};
void lcdc_reset_panel(void)
{
    gpio_set_direction(GPIO_LCD_RST, GPIO_OUT);
    gpio_output_set(GPIO_LCD_RST, 1);
    mdelay(10);
    gpio_output_set(GPIO_LCD_RST, 0);
    mdelay(10);
    gpio_output_set(GPIO_LCD_RST, 1);
    mdelay(100);
}
enum {
    SPI_WRITE_CMD = 0,
    SPI_WRITE_DATA,
    SPI_WRITE_DELAY
};

struct spi_write_t {
    uint8_t type;  /* SPI_WRITE_CMD / SPI_WRITE_DATA / SPI_WRITE_DELAY */
    uint8_t value;
    uint8_t data_length; /* Only for SPI_WRITE_CMD */
};

static int st7567_panel_init(unsigned width, unsigned height)
{
    int i = 0, j = 0;

    lcd_spi_write_cmd(0xE2,8);   //set softreset
    lcd_spi_write_cmd(0xF8,8);   //set booster
    lcd_spi_write_cmd(0x00,8);   //5
    lcd_spi_write_cmd(0x25,8);   //set resistor ratio
    lcd_spi_write_cmd(0x2F,8);   //select power circuit
    lcd_spi_write_cmd(0xA2,8);   //set bias: 1/9 bias
    lcd_spi_write_cmd(0x81,8);   //set EV
    lcd_spi_write_cmd(0x1B,8);   //contrast value 0x1B(27)
    //lcd_spi_write_cmd(0xA1,8);
    lcd_spi_write_cmd(0xA0,8);   //set SEG direction
    //lcd_spi_write_cmd(0xC0,8);
    lcd_spi_write_cmd(0xC8,8);  //set COM direction
    lcd_spi_write_cmd(0x40,8);   //start line adress
    //lcd_spi_write_cmd(0xA7,8);
    lcd_spi_write_cmd(0xA6,8);   //inverse
    lcd_spi_write_cmd(0xA4,8);
    lcd_spi_write_cmd(0xB0,8);   //page address

    for(i = 0; i < height; i = i+8){
        lcd_spi_write_cmd(0x10, 8);
        lcd_spi_write_cmd(0x00, 8);
        lcd_spi_write_cmd(0xB0+i/8, 8);

        for(j = 0; j < width; j++){
            lcd_spi_write_data(0xaa, 8);
        }
    }

    lcd_spi_write_cmd(0xaf, 8); // Display ON
    return 0;
}

const static struct spi_write_t spi_display_on_cmds[] = {
    { SPI_WRITE_CMD, 0xa4, 0 },
    { SPI_WRITE_CMD, 0xaf, 0 },
};

const static struct spi_write_t spi_display_off_cmds[] = {
    { SPI_WRITE_CMD, 0xae, 0 },
    { SPI_WRITE_CMD, 0xa5, 0 },
};

#define ID_TABLE_END 0

unsigned st7567_id_table[] = {
    0x7567,
    ID_TABLE_END,
};

static uint8_t suspend_flag = 0;
static void st7567_suspend(void *arg)
{
    struct crane_panel_t *p = arg;
    printf("st7735_suspend\n");
    /*since disp_refr can be changed by other register,
     *so if st7567 suspend need use lcd_disp_drv instead of lv_refr_vdb_is_flushing()
     *refer to lv_disp_refr_task() and lv_refr_vdb_flush()*/
    if(lcd_disp_drv && lcd_disp_drv->buffer) {
        // wait flushing finish~~
        while(lcd_disp_drv->buffer->flushing)
            ;
    }
    p->panel_onoff(0);
    suspend_flag = 1;
}

static int panel_detect(void);
static void st7567_resume(void *arg)
{
    struct crane_panel_t *p = arg;
    printf("st7735_resume\n");
    p->panel_onoff(1);
    suspend_flag = 0;

    lv_obj_invalidate(lv_disp_get_scr_act(NULL));
    lv_refr_now(_lv_refr_get_disp_refreshing());

    if(lcd_disp_drv && lcd_disp_drv->buffer) {
        // wait flushing finish~~
        int cnt = 0;
        while(lcd_disp_drv->buffer->flushing) {
            uos_sleep(1);
            cnt ++;
            if(cnt > 20) { // over 100ms
                int res = panel_detect();
                printf("st7567_resume fail!!  panel detect: %d\n", res);
                break;
            }
        }
    }
}

static void spi_write_function(const struct spi_write_t *spi_data, uint32_t length)
{
    uint16_t i = 0;
    uint8_t data_idx = 0;
    uint8_t data_length = 0;
    const struct spi_write_t *pdata = spi_data;

    if (pdata == NULL || length == 0) {
        cplog_printf("%s: %d: param is error!! \n", __FUNCTION__, __LINE__);
        return;
    }

    for (i = 0; i < length;) {
        i++;
        if (pdata->type == SPI_WRITE_CMD) {
            lcd_spi_write_cmd(pdata->value, 8);
            if (pdata->data_length > 0) {
                data_length = pdata->data_length;
                pdata++;
                for (data_idx = 0; (data_idx < data_length) && (i < length); data_idx++, i++) {
                    if (pdata->type != SPI_WRITE_DATA) {
                        cplog_printf("%s: %d: SPI data is error[%u, %u], please check!!!!!!!! \n",
                               __FUNCTION__, __LINE__, i, data_idx);
                        return;
                    }
                    else {
                        lcd_spi_write_data(pdata->value, 8);
                        pdata++;
                    }
                }
            }
            else {
                pdata++;
            }
        }
        else if (pdata->type == SPI_WRITE_DELAY) {
            mdelay(pdata->value);
            pdata++;
        }
        else {
            cplog_printf("%s: %d: SPI data is error[%u, %u], please check!!!!!!!! \n",
                   __FUNCTION__, __LINE__, i, pdata->type);
            return;
        }
    }

    return;
}

static void st7567_panel_invalid(uint32_t start_x,
                                  uint32_t start_y,  uint32_t end_x, uint32_t end_y)
{
    lcd_spi_write_cmd(0xB0 + pagemap[start_y / 8], 8);
    lcd_spi_write_cmd(0x10, 8);
    lcd_spi_write_cmd(0x0, 8);
}

static int match_id(unsigned id, unsigned *id_table)
{
    int found = 0;
    unsigned *p = id_table;

    while (*p) {
        if (id == *p) {
            found = 1;
            break;
        }
        p++;
    }

    return (found == 1) ? 1 : 0;
}

static int st7567_spi_read_id(unsigned *id)
{
    return st7567_id;
}

__attribute__ ((unused)) static int panel_detect_readid(void)
{
    unsigned id = 0;
    if (st7567_spi_read_id(&id) < 0) {
        cplog_printf("panel read id failed\n");
        goto err;
    }

    if (match_id(id, st7567_id_table) == 0) {
        cplog_printf("unknown panel id = 0x%x\n", id);
        goto err;
    }

    printf("Found LCD panel ST7567-SPI, id: 0x%x\n", id);
    printf("panel detect by readid ok\n");

    return 0;
err:
    cplog_printf("panel detect by readid failed\n");
    return -1;
}
__attribute__ ((unused)) static int panel_detect_fb(void)
{
    printf("LCD panel st7567: not support detected yet!!\n");
    return 0;
}

typedef int (*panel_detect_fn_t)(void);

static panel_detect_fn_t detect_fn[] = {
    panel_detect_readid,
    panel_detect_fb,
};

static int panel_detect(void)
{
    int cnt = ARRAY_SIZE(detect_fn);
    int i, ret;

    for (i = 0; i < cnt; i++) {
        ret = detect_fn[i]();
        if (ret == 0) {
            return 0;
        }
    }

    return -1;
}

static void st7567_spi_onoff(int on)
{
    uint32_t length;

    if (on) {
        length = sizeof(spi_display_on_cmds) / sizeof(struct spi_write_t);
        spi_write_function(spi_display_on_cmds, length);
    }
    else {   /* off */
        length = sizeof(spi_display_off_cmds) / sizeof(struct spi_write_t);
        spi_write_function(spi_display_off_cmds, length);
    }
}
__attribute__ ((unused)) static int st7567_spi_probe_poc(unsigned lcd_clk)
{
    lv_pm_info_t pm_info;

    lcd_spi_init(&lcd_info);

    lcdc_reset_panel();

    st7567_panel_init(LV_HOR_RES,LV_VER_RES);

    if (panel_detect() < 0) {
        return -1;
    }

    pm_info.suspend = st7567_suspend;
    pm_info.resume = st7567_resume;
    pm_info.data = &st7567_generic_spi;
    pm_info.part = LV_PM_PART_DISPLAY;
    lv_pm_register(&pm_info);

    return 0;
}

static void st7567_spi_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
    lcd_disp_drv = disp_drv;
    if(suspend_flag == 1) {
        lv_disp_flush_ready(disp_drv);
        return;
    }
    // printf("flush start(%d): %d \n", SPI_TRANSFER_MODE, lv_tick_get());
    uint32_t width = area->x2- area->x1 + 1;
    uint32_t height = area->y2 - area->y1 + 1;

    st7567_panel_invalid(area->x1, area->y1, area->x2, area->y2);
    //printf("flush : %d, %d -> %d, %d (%d, %d) \n", area->x1, area->y1, area->x2, area->y2, width, height);
    lcd_spi_write_fb((uint8_t *)color_p, width * height * 2, SPI_TRANSFER_MODE);
}

struct crane_panel_t st7567_generic_spi = {
    .probe = st7567_spi_probe_poc,
    .readid = st7567_spi_read_id,
    .panel_onoff = st7567_spi_onoff,
    .flush = st7567_spi_flush,
};

#endif

